/* src/interfaces/ecpg/ecpglib/connect.c */

#define POSTGRES_ECPG_INTERNAL
#include "postgres_fe.h"

#include "ecpg-pthread-win32.h"
#include "ecpgtype.h"
#include "ecpglib.h"
#include "ecpgerrno.h"
#include "extern.h"
#include "sqlca.h"

#ifdef ENABLE_THREAD_SAFETY
static pthread_mutex_t connections_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_key_t actual_connection_key;
static pthread_once_t actual_connection_key_once = PTHREAD_ONCE_INIT;
#endif
static struct connection* actual_connection = NULL;
static struct connection* all_connections = NULL;

#ifdef ENABLE_THREAD_SAFETY
static void ecpg_actual_connection_init(void)
{
    pthread_key_create(&actual_connection_key, NULL);
}

void ecpg_pthreads_init(void)
{
    pthread_once(&actual_connection_key_once, ecpg_actual_connection_init);
}
#endif

static struct connection* ecpg_get_connection_nr(const char* connection_name)
{
    struct connection* ret = NULL;

    if ((connection_name == NULL) || (strcmp(connection_name, "CURRENT") == 0)) {
#ifdef ENABLE_THREAD_SAFETY
        ret = (connection*)pthread_getspecific(actual_connection_key);

        /*
         * if no connection in TSD for this thread, get the global default
         * connection and hope the user knows what they're doing (i.e. using
         * their own mutex to protect that connection from concurrent accesses
         */
        /* if !ret then  we  got the connection from TSD */
        if (NULL == ret)
            /* no TSD connection, going for global */
            ret = actual_connection;
#else
        ret = actual_connection;
#endif
    } else {
        struct connection* con;

        for (con = all_connections; con != NULL; con = con->next) {
            if (strcmp(connection_name, con->name) == 0)
                break;
        }
        ret = con;
    }

    return (ret);
}

struct connection* ecpg_get_connection(const char* connection_name)
{
    struct connection* ret = NULL;

    if ((connection_name == NULL) || (strcmp(connection_name, "CURRENT") == 0)) {
#ifdef ENABLE_THREAD_SAFETY
        ret = (connection*)pthread_getspecific(actual_connection_key);

        /*
         * if no connection in TSD for this thread, get the global default
         * connection and hope the user knows what they're doing (i.e. using
         * their own mutex to protect that connection from concurrent accesses
         */
        /* if !ret then  we  got the connection from TSD */
        if (NULL == ret)
            /* no TSD connection here either, using global */
            ret = actual_connection;
#else
        ret = actual_connection;
#endif
    } else {
#ifdef ENABLE_THREAD_SAFETY
        pthread_mutex_lock(&connections_mutex);
#endif

        ret = ecpg_get_connection_nr(connection_name);

#ifdef ENABLE_THREAD_SAFETY
        pthread_mutex_unlock(&connections_mutex);
#endif
    }

    return (ret);
}

static void ecpg_finish(struct connection* act)
{
    if (act != NULL) {
        struct ECPGtype_information_cache *cache, *ptr;

        ecpg_deallocate_all_conn(0, ECPG_COMPAT_PGSQL, act);
        PQfinish(act->connection);

        /*
         * no need to lock connections_mutex - we're always called by
         * ECPGdisconnect or ECPGconnect, which are holding the lock
         */

        /* remove act from the list */
        if (act == all_connections)
            all_connections = act->next;
        else {
            struct connection* con;

            for (con = all_connections; (con->next != NULL) && con->next != act; con = con->next)
                ;
            if (con->next != NULL)
                con->next = act->next;
        }

#ifdef ENABLE_THREAD_SAFETY
        if (pthread_getspecific(actual_connection_key) == act)
            pthread_setspecific(actual_connection_key, all_connections);
#endif
        if (actual_connection == act)
            actual_connection = all_connections;

        ecpg_log("ecpg_finish: connection %s closed\n", act->name != NULL ? act->name : "(null)");

        for (cache = act->cache_head; cache != NULL; ptr = cache, cache = cache->next, ecpg_free(ptr))
            ;
        ecpg_free(act->name);
        ecpg_free(act);
        /* delete cursor variables when last connection gets closed */
        if (all_connections == NULL) {
            struct var_list* iv_ptr;

            for (; ivlist != NULL; iv_ptr = ivlist, ivlist = ivlist->next, ecpg_free(iv_ptr))
                ;
        }
    } else
        ecpg_log("ecpg_finish: called an extra time\n");
}

bool ECPGsetcommit(int lineno, const char* mode, const char* connection_name)
{
    struct connection* con = ecpg_get_connection(connection_name);
    PGresult* results = NULL;

    if (!ecpg_init(con, connection_name, lineno))
        return (false);

    ecpg_log("ECPGsetcommit on line %d: action \"%s\"; connection \"%s\"\n", lineno, mode, con->name);

    if (con->autocommit && strncmp(mode, "off", strlen("off")) == 0) {
        if (PQtransactionStatus(con->connection) == PQTRANS_IDLE) {
            results = PQexec(con->connection, "begin transaction");
            if (!ecpg_check_PQresult(results, lineno, con->connection, ECPG_COMPAT_PGSQL))
                return false;
            PQclear(results);
        }
        con->autocommit = false;
    } else if (!con->autocommit && strncmp(mode, "on", strlen("on")) == 0) {
        if (PQtransactionStatus(con->connection) != PQTRANS_IDLE) {
            results = PQexec(con->connection, "commit");
            if (!ecpg_check_PQresult(results, lineno, con->connection, ECPG_COMPAT_PGSQL))
                return false;
            PQclear(results);
        }
        con->autocommit = true;
    }

    return true;
}

bool ECPGsetconn(int lineno, const char* connection_name)
{
    struct connection* con = ecpg_get_connection(connection_name);

    if (!ecpg_init(con, connection_name, lineno))
        return (false);

#ifdef ENABLE_THREAD_SAFETY
    pthread_setspecific(actual_connection_key, con);
#else
    actual_connection = con;
#endif
    return true;
}

static void ECPGnoticeReceiver(void* arg, const PGresult* result)
{
    char* sqlstate = PQresultErrorField(result, PG_DIAG_SQLSTATE);
    char* message = PQresultErrorField(result, PG_DIAG_MESSAGE_PRIMARY);
    struct sqlca_t* sqlca = ECPGget_sqlca();
    int sqlcode;

    (void)arg; /* keep the compiler quiet */
    if (sqlstate == NULL)
        sqlstate = ECPG_SQLSTATE_ECPG_INTERNAL_ERROR;

    if (message == NULL) /* Shouldn't happen, but need to be sure */
        message = ecpg_gettext("empty message text");

    /* these are not warnings */
    if (strncmp(sqlstate, "00", 2) == 0)
        return;

    ecpg_log("ECPGnoticeReceiver: %s\n", message);

    /* map to SQLCODE for backward compatibility */
    if (strcmp(sqlstate, ECPG_SQLSTATE_INVALID_CURSOR_NAME) == 0)
        sqlcode = ECPG_WARNING_UNKNOWN_PORTAL;
    else if (strcmp(sqlstate, ECPG_SQLSTATE_ACTIVE_SQL_TRANSACTION) == 0)
        sqlcode = ECPG_WARNING_IN_TRANSACTION;
    else if (strcmp(sqlstate, ECPG_SQLSTATE_NO_ACTIVE_SQL_TRANSACTION) == 0)
        sqlcode = ECPG_WARNING_NO_TRANSACTION;
    else if (strcmp(sqlstate, ECPG_SQLSTATE_DUPLICATE_CURSOR) == 0)
        sqlcode = ECPG_WARNING_PORTAL_EXISTS;
    else
        sqlcode = 0;

    strncpy(sqlca->sqlstate, sqlstate, sizeof(sqlca->sqlstate));
    sqlca->sqlcode = sqlcode;
    sqlca->sqlwarn[2] = 'W';
    sqlca->sqlwarn[0] = 'W';

    strncpy(sqlca->sqlerrm.sqlerrmc, message, sizeof(sqlca->sqlerrm.sqlerrmc) - 1);
    sqlca->sqlerrm.sqlerrmc[sizeof(sqlca->sqlerrm.sqlerrmc) - 1] = 0;
    sqlca->sqlerrm.sqlerrml = strlen(sqlca->sqlerrm.sqlerrmc);

    ecpg_log("raising sqlcode %d\n", sqlcode);
}

/* this contains some quick hacks, needs to be cleaned up, but it works */
bool ECPGconnect(int lineno, int c, const char* name, const char* user, const char* passwd, const char* connection_name,
    int autocommit)
{
    struct sqlca_t* sqlca = ECPGget_sqlca();
    enum COMPAT_MODE compat = (COMPAT_MODE)c;
    struct connection* thisPtr;
    int i, connect_params = 0;
    char *dbname = name != NULL ? ecpg_strdup(name, lineno) : NULL, *host = NULL, *tmp, *port = NULL, *realname = NULL,
         *options = NULL;
    const char** conn_keywords;
    const char** conn_values;

    ecpg_init_sqlca(sqlca);

    /*
     * clear auto_mem structure because some error handling functions might
     * access it
     */
    ecpg_clear_auto_mem();

    if (INFORMIX_MODE(compat)) {
        char* envname = NULL;

        /*
         * Informix uses an environment variable DBPATH that overrides the
         * connection parameters given here. We do the same with PG_DBPATH as
         * the syntax is different.
         */
        envname = getenv("PG_DBPATH");
        if (envname != NULL) {
            ecpg_free(dbname);
            dbname = ecpg_strdup(envname, lineno);
        }
    }

    if (dbname == NULL && connection_name == NULL)
        connection_name = "DEFAULT";

#if ENABLE_THREAD_SAFETY
    ecpg_pthreads_init();
#endif

    /* check if the identifier is unique */
    if (ecpg_get_connection(connection_name) != NULL) {
        ecpg_free(dbname);
        ecpg_log("ECPGconnect: connection identifier %s is already in use\n", connection_name);
        return false;
    }

    if ((thisPtr = (struct connection*)ecpg_alloc(sizeof(struct connection), lineno)) == NULL) {
        ecpg_free(dbname);
        return false;
    }

    if (dbname != NULL) {
        /* get the detail information out of dbname */
        if (strncmp(dbname, "tcp:", 4) == 0 || strncmp(dbname, "unix:", 5) == 0) {
            int offset = 0;

            /*
             * only allow protocols tcp and unix
             */
            if (strncmp(dbname, "tcp:", 4) == 0)
                offset = 4;
            else if (strncmp(dbname, "unix:", 5) == 0)
                offset = 5;

            if (strncmp(dbname + offset, "postgresql://", strlen("postgresql://")) == 0) {

                /* ------
                 * new style:
                 *	<tcp|unix>:postgresql://server[:port|:/unixsocket/path:]
                 *	[/db name][?options]
                 * ------
                 */
                offset += strlen("postgresql://");

                tmp = strrchr(dbname + offset, '?');
                if (tmp != NULL) /* options given */
                {
                    options = ecpg_strdup(tmp + 1, lineno);
                    *tmp = '\0';
                }

                tmp = last_dir_separator(dbname + offset);
                if (tmp != NULL) /* database name given */
                {
                    if (tmp[1] != '\0') /* non-empty database name */
                    {
                        realname = ecpg_strdup(tmp + 1, lineno);
                        connect_params++;
                    }
                    *tmp = '\0';
                }

                tmp = strrchr(dbname + offset, ':');
                if (tmp != NULL) /* port number or Unix socket path given */
                {
                    char* tmp2 = NULL;

                    *tmp = '\0';
                    if ((tmp2 = strchr(tmp + 1, ':')) != NULL) {
                        *tmp2 = '\0';
                        host = ecpg_strdup(tmp + 1, lineno);
                        connect_params++;
                        if (strncmp(dbname, "unix:", 5) != 0) {
                            ecpg_log("ECPGconnect: socketname %s given for TCP connection on line %d\n", host, lineno);
                            ecpg_raise(lineno,
                                ECPG_CONNECT,
                                ECPG_SQLSTATE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
                                realname != NULL ? realname : ecpg_gettext("<DEFAULT>"));
                            if (host != NULL)
                                ecpg_free(host);

                            /*
                             * port not set yet if (port) ecpg_free(port);
                             */
                            if (options != NULL)
                                ecpg_free(options);
                            if (realname != NULL)
                                ecpg_free(realname);
                            if (dbname != NULL)
                                ecpg_free(dbname);
                            free(thisPtr);
                            return false;
                        }
                    } else {
                        port = ecpg_strdup(tmp + 1, lineno);
                        connect_params++;
                    }
                }

                if (strncmp(dbname, "unix:", 5) == 0) {
                    if (strcmp(dbname + offset, "localhost") != 0 && strcmp(dbname + offset, "127.0.0.1") != 0) {
                        ecpg_log("ECPGconnect: non-localhost access via sockets on line %d\n", lineno);
                        ecpg_raise(lineno,
                            ECPG_CONNECT,
                            ECPG_SQLSTATE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
                            realname != NULL ? realname : ecpg_gettext("<DEFAULT>"));
                        if (host != NULL)
                            ecpg_free(host);
                        if (port != NULL)
                            ecpg_free(port);
                        if (options != NULL)
                            ecpg_free(options);
                        if (realname != NULL)
                            ecpg_free(realname);
                        if (dbname != NULL)
                            ecpg_free(dbname);
                        free(thisPtr);
                        return false;
                    }
                } else {
                    host = ecpg_strdup(dbname + offset, lineno);
                    connect_params++;
                }
            }
        } else {
            /* old style: dbname[@server][:port] */
            tmp = strrchr(dbname, ':');
            if (tmp != NULL) /* port number given */
            {
                port = ecpg_strdup(tmp + 1, lineno);
                connect_params++;
                *tmp = '\0';
            }

            tmp = strrchr(dbname, '@');
            if (tmp != NULL) /* host name given */
            {
                host = ecpg_strdup(tmp + 1, lineno);
                connect_params++;
                *tmp = '\0';
            }

            if (strlen(dbname) > 0) {
                realname = ecpg_strdup(dbname, lineno);
                connect_params++;
            } else
                realname = NULL;
        }
    } else
        realname = NULL;

        /* add connection to our list */
#ifdef ENABLE_THREAD_SAFETY
    pthread_mutex_lock(&connections_mutex);
#endif
    if (connection_name != NULL)
        thisPtr->name = ecpg_strdup(connection_name, lineno);
    else
        thisPtr->name = ecpg_strdup(realname, lineno);

    thisPtr->cache_head = NULL;
    thisPtr->prep_stmts = NULL;

    if (all_connections == NULL)
        thisPtr->next = NULL;
    else
        thisPtr->next = all_connections;

    all_connections = thisPtr;
#ifdef ENABLE_THREAD_SAFETY
    pthread_setspecific(actual_connection_key, all_connections);
#endif
    actual_connection = all_connections;

    ecpg_log("ECPGconnect: opening database %s on %s port %s %s%s %s%s\n",
        realname != NULL ? realname : "<DEFAULT>",
        host != NULL ? host : "<DEFAULT>",
        port != NULL ? (ecpg_internal_regression_mode ? "<REGRESSION_PORT>" : port) : "<DEFAULT>",
        options != NULL ? "with options " : "",
        options != NULL ? options : "",
        ((user != NULL) && strlen(user) > 0) ? "for user " : "",
        user != NULL ? user : "");

    if (options != NULL)
        for (i = 0; options[i]; i++)
            /* count options */
            if (options[i] == '=')
                connect_params++;

    if ((user != NULL) && strlen(user) > 0)
        connect_params++;
    if ((passwd != NULL) && strlen(passwd) > 0)
        connect_params++;

    /* allocate enough space for all connection parameters */
    conn_keywords = (const char**)ecpg_alloc((connect_params + 1) * sizeof(char*), lineno);
    conn_values = (const char**)ecpg_alloc(connect_params * sizeof(char*), lineno);
    if (conn_keywords == NULL || conn_values == NULL) {
        if (host != NULL)
            ecpg_free(host);
        if (port != NULL)
            ecpg_free(port);
        if (options != NULL)
            ecpg_free(options);
        if (realname != NULL)
            ecpg_free(realname);
        if (dbname != NULL)
            ecpg_free(dbname);
        if (conn_keywords != NULL)
            ecpg_free(conn_keywords);
        if (conn_values != NULL)
            ecpg_free(conn_values);
        free(thisPtr);
        return false;
    }

    i = 0;
    if (realname != NULL) {
        conn_keywords[i] = "dbname";
        conn_values[i] = realname;
        i++;
    }
    if (host != NULL) {
        conn_keywords[i] = "host";
        conn_values[i] = host;
        i++;
    }
    if (port != NULL) {
        conn_keywords[i] = "port";
        conn_values[i] = port;
        i++;
    }
    if ((user != NULL) && strlen(user) > 0) {
        conn_keywords[i] = "user";
        conn_values[i] = user;
        i++;
    }
    if ((passwd != NULL) && strlen(passwd) > 0) {
        conn_keywords[i] = "password";
        conn_values[i] = passwd;
        i++;
    }
    if (options != NULL) {
        char* str = NULL;

        /* options look like this "option1 = value1 option2 = value2 ... */
        /* we have to break up the string into single options */
        for (str = options; *str;) {
            int e, a;
            char *token1, *token2;

            for (token1 = str; *token1 && *token1 == ' '; token1++)
                ;
            for (e = 0; token1[e] && token1[e] != '='; e++)
                ;
            if (token1[e]) /* found "=" */
            {
                token1[e] = '\0';
                for (token2 = token1 + e + 1; *token2 && *token2 == ' '; token2++)
                    ;
                for (a = 0; token2[a] && token2[a] != '&'; a++)
                    ;
                if (token2[a]) /* found "&" => another option follows */
                {
                    token2[a] = '\0';
                    str = token2 + a + 1;
                } else
                    str = token2 + a;

                conn_keywords[i] = token1;
                conn_values[i] = token2;
                i++;
            } else
                /* the parser should not be able to create this invalid option */
                str = token1 + e;
        }
    }
    conn_keywords[i] = NULL; /* terminator */

    thisPtr->connection = PQconnectdbParams(conn_keywords, conn_values, 0);

    if (host != NULL)
        ecpg_free(host);
    if (port != NULL)
        ecpg_free(port);
    if (options != NULL)
        ecpg_free(options);
    if (dbname != NULL)
        ecpg_free(dbname);
    ecpg_free(conn_values);
    ecpg_free(conn_keywords);

    if (PQstatus(thisPtr->connection) == CONNECTION_BAD) {
        const char* errmsg = PQerrorMessage(thisPtr->connection);
        const char* db = realname != NULL ? realname : ecpg_gettext("<DEFAULT>");

        ecpg_log("ECPGconnect: could not open database: %s\n", errmsg);

        ecpg_finish(thisPtr);
#ifdef ENABLE_THREAD_SAFETY
        pthread_mutex_unlock(&connections_mutex);
#endif

        ecpg_raise(lineno, ECPG_CONNECT, ECPG_SQLSTATE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION, db);
        if (realname != NULL)
            ecpg_free(realname);

        return false;
    }

    if (realname != NULL)
        ecpg_free(realname);

#ifdef ENABLE_THREAD_SAFETY
    pthread_mutex_unlock(&connections_mutex);
#endif

    thisPtr->autocommit = autocommit;

    PQsetNoticeReceiver(thisPtr->connection, &ECPGnoticeReceiver, (void*)thisPtr);

    return true;
}

bool ECPGdisconnect(int lineno, const char* connection_name)
{
    struct sqlca_t* sqlca = ECPGget_sqlca();
    struct connection* con;

#ifdef ENABLE_THREAD_SAFETY
    pthread_mutex_lock(&connections_mutex);
#endif

    if (strcmp(connection_name, "ALL") == 0) {
        ecpg_init_sqlca(sqlca);
        for (con = all_connections; con != NULL;) {
            struct connection* f = con;

            con = con->next;
            ecpg_finish(f);
        }
    } else {
        con = ecpg_get_connection_nr(connection_name);

        if (!ecpg_init(con, connection_name, lineno)) {
#ifdef ENABLE_THREAD_SAFETY
            pthread_mutex_unlock(&connections_mutex);
#endif
            return (false);
        } else
            ecpg_finish(con);
    }

#ifdef ENABLE_THREAD_SAFETY
    pthread_mutex_unlock(&connections_mutex);
#endif

    return true;
}

PGconn* ECPGget_PGconn(const char* connection_name)
{
    struct connection* con;

    con = ecpg_get_connection(connection_name);
    if (con == NULL)
        return NULL;

    return con->connection;
}
